msg_tool\scripts\cat_system\archive/
int.rs

1//! CatSystem2 Archive File (.int)
2use super::twister::MersenneTwister;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::blowfish::Blowfish;
7use crate::utils::crc32::CRC32NORMAL_TABLE;
8use crate::utils::encoding::{decode_to_string, encode_string};
9use anyhow::Result;
10use overf::wrapping;
11use std::io::{Read, Seek, SeekFrom};
12use std::sync::{Arc, Mutex};
13
14pub use super::int_password::get_password_from_exe;
15
16#[derive(Debug)]
17/// Builder for CatSystem2 Archive scripts.
18pub struct CSIntArcBuilder {}
19
20impl CSIntArcBuilder {
21    /// Creates a new instance of `CSIntArcBuilder`.
22    pub fn new() -> Self {
23        CSIntArcBuilder {}
24    }
25}
26
27impl ScriptBuilder for CSIntArcBuilder {
28    fn default_encoding(&self) -> Encoding {
29        Encoding::Cp932
30    }
31
32    fn default_archive_encoding(&self) -> Option<Encoding> {
33        Some(Encoding::Cp932)
34    }
35
36    fn build_script(
37        &self,
38        data: Vec<u8>,
39        filename: &str,
40        _encoding: Encoding,
41        archive_encoding: Encoding,
42        config: &ExtraConfig,
43        _archive: Option<&Box<dyn Script>>,
44    ) -> Result<Box<dyn Script>> {
45        Ok(Box::new(CSIntArc::new(
46            MemReader::new(data),
47            archive_encoding,
48            config,
49            filename,
50        )?))
51    }
52
53    fn build_script_from_file(
54        &self,
55        filename: &str,
56        _encoding: Encoding,
57        archive_encoding: Encoding,
58        config: &ExtraConfig,
59        _archive: Option<&Box<dyn Script>>,
60    ) -> Result<Box<dyn Script>> {
61        if filename == "-" {
62            let data = crate::utils::files::read_file(filename)?;
63            Ok(Box::new(CSIntArc::new(
64                MemReader::new(data),
65                archive_encoding,
66                config,
67                filename,
68            )?))
69        } else {
70            let f = std::fs::File::open(filename)?;
71            let reader = std::io::BufReader::new(f);
72            Ok(Box::new(CSIntArc::new(
73                reader,
74                archive_encoding,
75                config,
76                filename,
77            )?))
78        }
79    }
80
81    fn build_script_from_reader(
82        &self,
83        reader: Box<dyn ReadSeek>,
84        filename: &str,
85        _encoding: Encoding,
86        archive_encoding: Encoding,
87        config: &ExtraConfig,
88        _archive: Option<&Box<dyn Script>>,
89    ) -> Result<Box<dyn Script>> {
90        Ok(Box::new(CSIntArc::new(
91            reader,
92            archive_encoding,
93            config,
94            filename,
95        )?))
96    }
97
98    fn extensions(&self) -> &'static [&'static str] {
99        &["int"]
100    }
101
102    fn script_type(&self) -> &'static ScriptType {
103        &ScriptType::CatSystemInt
104    }
105
106    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
107        if buf_len >= 4 && buf.starts_with(b"KIF\0") {
108            return Some(10);
109        }
110        None
111    }
112
113    fn is_archive(&self) -> bool {
114        true
115    }
116}
117
118fn detect_script_type(buf: &[u8], buf_len: usize, _filename: &str) -> Option<&'static ScriptType> {
119    #[cfg(feature = "cat-system-img")]
120    if buf_len >= 4 && buf.starts_with(b"HG-3") {
121        return Some(&ScriptType::CatSystemHg3);
122    }
123    if buf_len >= 8 && buf.starts_with(b"CatScene") {
124        return Some(&ScriptType::CatSystem);
125    }
126    if buf_len >= 4 && buf.starts_with(b"CSTL") {
127        return Some(&ScriptType::CatSystemCstl);
128    }
129    None
130}
131
132#[derive(Clone, Debug)]
133struct CSIntFileHeader {
134    name: String,
135    offset: u32,
136    size: u32,
137}
138
139struct Entry<T: Read + Seek> {
140    header: CSIntFileHeader,
141    reader: Arc<Mutex<T>>,
142    pos: usize,
143    script_type: Option<ScriptType>,
144}
145
146impl<T: Read + Seek> ArchiveContent for Entry<T> {
147    fn name(&self) -> &str {
148        &self.header.name
149    }
150
151    fn script_type(&self) -> Option<&ScriptType> {
152        self.script_type.as_ref()
153    }
154}
155
156impl<T: Read + Seek> Read for Entry<T> {
157    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
158        let mut reader = self.reader.lock().map_err(|e| {
159            std::io::Error::new(
160                std::io::ErrorKind::Other,
161                format!("Failed to lock mutex: {}", e),
162            )
163        })?;
164        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
165        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
166        if bytes_read == 0 {
167            return Ok(0);
168        }
169        let bytes_read = reader.read(&mut buf[..bytes_read])?;
170        self.pos += bytes_read;
171        Ok(bytes_read)
172    }
173}
174
175impl<T: Read + Seek> Seek for Entry<T> {
176    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
177        let new_pos = match pos {
178            SeekFrom::Start(offset) => offset as usize,
179            SeekFrom::End(offset) => {
180                if offset < 0 {
181                    if (-offset) as usize > self.header.size as usize {
182                        return Err(std::io::Error::new(
183                            std::io::ErrorKind::InvalidInput,
184                            "Seek from end exceeds file length",
185                        ));
186                    }
187                    self.header.size as usize - (-offset) as usize
188                } else {
189                    self.header.size as usize + offset as usize
190                }
191            }
192            SeekFrom::Current(offset) => {
193                if offset < 0 {
194                    if (-offset) as usize > self.pos {
195                        return Err(std::io::Error::new(
196                            std::io::ErrorKind::InvalidInput,
197                            "Seek from current exceeds current position",
198                        ));
199                    }
200                    self.pos.saturating_sub((-offset) as usize)
201                } else {
202                    self.pos + offset as usize
203                }
204            }
205        };
206        self.pos = new_pos;
207        Ok(self.pos as u64)
208    }
209
210    fn stream_position(&mut self) -> std::io::Result<u64> {
211        Ok(self.pos as u64)
212    }
213}
214
215struct MemEntry {
216    name: String,
217    data: MemReader,
218}
219
220impl Read for MemEntry {
221    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
222        self.data.read(buf)
223    }
224}
225
226impl ArchiveContent for MemEntry {
227    fn name(&self) -> &str {
228        &self.name
229    }
230
231    fn script_type(&self) -> Option<&ScriptType> {
232        detect_script_type(&self.data.data, self.data.data.len(), &self.name)
233    }
234
235    fn data(&mut self) -> Result<Vec<u8>> {
236        Ok(self.data.data.clone())
237    }
238
239    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
240        Ok(Box::new(&mut self.data))
241    }
242}
243
244#[derive(Debug)]
245/// CatSystem2 Archive script.
246pub struct CSIntArc<T: Read + Seek + std::fmt::Debug> {
247    reader: Arc<Mutex<T>>,
248    encrypt: Option<Blowfish>,
249    entries: Vec<CSIntFileHeader>,
250}
251
252const NAME_SIZES: [usize; 2] = [0x20, 0x40];
253
254impl<T: Read + Seek + std::fmt::Debug> CSIntArc<T> {
255    /// Creates a new instance of `CSIntArc` from a reader.
256    ///
257    /// * `reader` - The reader to read the archive from.
258    /// * `archive_encoding` - The encoding used for the archive.
259    /// * `config` - Extra configuration options.
260    /// * `filename` - The name of the file.
261    pub fn new(
262        mut reader: T,
263        archive_encoding: Encoding,
264        config: &ExtraConfig,
265        _filename: &str,
266    ) -> Result<Self> {
267        let mut magic = [0u8; 4];
268        reader.read_exact(&mut magic)?;
269        if &magic != b"KIF\0" {
270            return Err(anyhow::anyhow!(
271                "Invalid magic number for CatSystem2 archive"
272            ));
273        }
274        let entry_count = reader.read_u32()?;
275        let mut keybuf = [0u8; 12];
276        reader.read_exact(&mut keybuf)?;
277        if &keybuf == b"__key__.dat\0" {
278            let key = match &config.cat_system_int_encrypt_password {
279                Some(password) => Self::get_key(password)?,
280                None => {
281                    return Err(anyhow::anyhow!(
282                        "CatSystem2 archive requires encryption password. Please use --cat-system-int-encrypt-password/--cat-system-int-exe option."
283                    ));
284                }
285            };
286            eprintln!("Using CatSystem2 archive encryption key: {key:08X}");
287            let seed = reader.peek_u32_at(0x4C)?;
288            let mut twister = MersenneTwister::new(seed);
289            let blowfish_key = twister.rand().to_le_bytes();
290            let encrypt = match Blowfish::new(&blowfish_key) {
291                Ok(bf) => bf,
292                Err(e) => {
293                    return Err(anyhow::anyhow!("Failed to create Blowfish cipher: {}", e));
294                }
295            };
296            let mut entries = Vec::with_capacity(entry_count as usize - 1);
297            let mut name_buf = [0u8; 0x40];
298            reader.seek(SeekFrom::Start(0x50))?;
299            for i in 1..entry_count {
300                reader.read_exact(&mut name_buf)?;
301                let offset = reader.read_u32()? + i;
302                let size = reader.read_u32()?;
303                let decryped = encrypt.decrypt([offset, size]);
304                twister.s_rand(key + i);
305                let name_key = twister.rand();
306                let name = Self::decrypt_name(&mut name_buf, name_key, archive_encoding)?;
307                let entry = CSIntFileHeader {
308                    name,
309                    offset: decryped[0],
310                    size: decryped[1],
311                };
312                entries.push(entry);
313            }
314            return Ok(CSIntArc {
315                reader: Arc::new(Mutex::new(reader)),
316                encrypt: Some(encrypt),
317                entries: entries,
318            });
319        }
320        let file_size = reader.seek(SeekFrom::End(0))?;
321        let mut entries = Vec::with_capacity(entry_count as usize);
322        for size in NAME_SIZES {
323            reader.seek(SeekFrom::Start(0x8))?;
324            for _ in 0..entry_count {
325                let name = reader.read_fstring(size, archive_encoding, true)?;
326                if name.is_empty() {
327                    entries.clear();
328                    break;
329                }
330                let current_offset = reader.stream_position()?;
331                let offset = reader.read_u32()?;
332                let size = reader.read_u32()?;
333                if offset as u64 <= current_offset
334                    || !((offset as u64) < file_size
335                        && size as u64 <= file_size
336                        && offset as u64 <= file_size as u64 - size as u64)
337                {
338                    entries.clear();
339                    break;
340                }
341                let entry = CSIntFileHeader { name, offset, size };
342                entries.push(entry);
343            }
344            if !entries.is_empty() {
345                return Ok(CSIntArc {
346                    reader: Arc::new(Mutex::new(reader)),
347                    encrypt: None,
348                    entries,
349                });
350            }
351        }
352        Err(anyhow::anyhow!(
353            "Failed to parse archives. Maybe another name length is used? (expected 0x20 or 0x40)",
354        ))
355    }
356
357    fn decrypt_name(name: &mut [u8; 0x40], key: u32, encoding: Encoding) -> Result<String> {
358        let mut k = wrapping! {((key >> 24) + (key >> 16) + (key >> 8) + key) & 0xFF};
359        let mut i = 0;
360        while i < 0x40 && name[i] != 0 {
361            let v = name[i];
362            if v.is_ascii_alphabetic() {
363                let mut j = if v.is_ascii_lowercase() {
364                    b'z' - v
365                } else {
366                    b'Z' - v + 26
367                } as i8;
368                j -= (k % 0x34) as i8;
369                if j < 0 {
370                    j += 0x34;
371                }
372                j = 0x33 - j;
373                name[i] = if j < 26 {
374                    b'z' - j as u8
375                } else {
376                    b'Z' - (j as u8 - 26)
377                };
378            }
379            k += 1;
380            i += 1;
381        }
382        decode_to_string(encoding, &name[..i], true)
383    }
384
385    fn get_key(password: &str) -> Result<u32> {
386        let bytes = encode_string(Encoding::Cp932, password, true)?;
387        let mut key = 0xFFFFFFFF;
388        for &c in bytes.iter() {
389            key = !CRC32NORMAL_TABLE[((key >> 24) ^ c as u32) as usize] ^ (key << 8);
390        }
391        Ok(key)
392    }
393}
394
395impl<T: Read + Seek + std::fmt::Debug + 'static> Script for CSIntArc<T> {
396    fn default_output_script_type(&self) -> OutputScriptType {
397        OutputScriptType::Json
398    }
399
400    fn default_format_type(&self) -> FormatOptions {
401        FormatOptions::None
402    }
403
404    fn is_archive(&self) -> bool {
405        true
406    }
407
408    fn iter_archive_filename<'a>(
409        &'a self,
410    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
411        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
412    }
413
414    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
415        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
416    }
417
418    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
419        if index >= self.entries.len() {
420            return Err(anyhow::anyhow!(
421                "Index out of bounds: {} (max: {})",
422                index,
423                self.entries.len()
424            ));
425        }
426        let entry = &self.entries[index];
427        let mut entry = Entry {
428            header: entry.clone(),
429            reader: self.reader.clone(),
430            pos: 0,
431            script_type: None,
432        };
433        if let Some(encrypt) = &self.encrypt {
434            let mut data = entry.data()?;
435            entry.pos = 0;
436            for i in 0..data.len() / 8 {
437                let j = i * 8;
438                let l = data[j] as u32
439                    | (data[j + 1] as u32) << 8
440                    | (data[j + 2] as u32) << 16
441                    | (data[j + 3] as u32) << 24;
442                let r = data[j + 4] as u32
443                    | (data[j + 5] as u32) << 8
444                    | (data[j + 6] as u32) << 16
445                    | (data[j + 7] as u32) << 24;
446                let result = encrypt.decrypt([l, r]);
447                data[j..j + 4].copy_from_slice(&result[0].to_le_bytes());
448                data[j + 4..j + 8].copy_from_slice(&result[1].to_le_bytes());
449            }
450            return Ok(Box::new(MemEntry {
451                name: entry.header.name.clone(),
452                data: MemReader::new(data),
453            }));
454        }
455        let mut buf = [0u8; 32];
456        let buf_len = match entry.read(&mut buf) {
457            Ok(len) => len,
458            Err(e) => {
459                return Err(anyhow::anyhow!(
460                    "Failed to read entry '{}': {}",
461                    entry.header.name,
462                    e
463                ));
464            }
465        };
466        entry.pos = 0;
467        entry.script_type = detect_script_type(&buf, buf_len, &entry.header.name).copied();
468        Ok(Box::new(entry))
469    }
470}